Introduce serial and parallel caster modes with SoA simulation architecture#39
Introduce serial and parallel caster modes with SoA simulation architecture#39weenachuangkud wants to merge 63 commits into
Conversation
- ActiveCastSerial: simplified, uses SerialSimulation - BaseCastSerial: uses SerialSimulation for all casts - SerialSimulation: single RunService, SoA, queue technique like SwiftCast
- Add ParallelSimulation: SoA pattern for each Actor - Update BaseCast: uses ParallelSimulation instead of per-cast Heartbeat - Each Actor now has one RunService handling multiple casts
- ActiveCast: Remove UpdateConnection (old per-cast heartbeat) - ActiveCastSerial: Remove metatable, use pure data structure - Now internal uses SoA, external exposes OOP API
- Add Motor6DPool for efficient Motor6D pooling - Add MovementMethod to FastCastBehavior (BulkMoveTo/Transform) - Update SerialSimulation to support Transform mode - Per-caster configuration, not global
- Update ParallelSimulation with Motor6D Transform support - Same as SerialSimulation implementation
- Added 0.1.0 changelog entry documenting new features - Created Serial benchmark (benchSerial.client.luau) - Created Parallel benchmark (benchParallel.client.luau) - Updated TODO.md with all completed items
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughFastCast2 is refactored into distinct Serial and Parallel casters with SoA-driven SerialSimulation and ParallelSimulation. Trajectory storage consolidates to a single ChangesSerial and Parallel Casting Architecture Refactor with SoA Simulation
Sequence Diagram(s)sequenceDiagram
participant User
participant Bench as Benchmark Script
participant Caster as FastCast Caster
participant Sim as Serial/Parallel Simulation
User->>Bench: press E or P
Bench->>Caster: Fire AMOUNT casts
Caster->>Sim: Register casts
Sim->>Sim: process casts per frame
Bench->>Caster: Terminate casts after BENCH_TIME
Bench->>User: print FPS stats
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
src/FastCast2/BaseCast.luau (1)
175-191:⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy liftCasts registered with
ParallelSimulationbut not stored inActivestable breaks cleanup and sync systems.The cast created at lines 175-191 is registered with
ParallelSimulation.Register(cast)but never added to theActivestable. However,Activesis still expected by:
HandleBulkMoveTo(line 56): IteratesActivesto batch-update cosmetic bullet positionsActiveCastCleaner.Event(line 127): ExpectsActives[activeCastID]to remove cleaned castsSyncChanges.Event(line 139): ExpectsActives[ID]to sync cast stateDestroy(line 368): IteratesActivesto terminate remaining castsAdditionally,
ParallelSimulation.Terminate()does not fire theActiveCastCleanerevent when a cast completes. The cast object is passed both cleanup and sync event references during creation, but these events will never be triggered by the parallel simulation, leaving cleanup callbacks unreachable.The same pattern exists in
Blockcast(lines 238-255) andSpherecast(lines 282-299).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/FastCast2/BaseCast.luau` around lines 175 - 191, The new casts (created in the Raycast/Blockcast/Spherecast paths using ActiveCast.createCastData and then ParallelSimulation.Register(cast)) must be added into the Actives table and have their completion trigger the existing cleanup/sync events; after you call ParallelSimulation.Register(cast) add Actives[cast.ID] = cast, and ensure when the parallel simulation signals a cast completion you fire ActiveCastCleaner.Event(cast.ID) (and remove Actives[cast.ID]) and also fire SyncChanges.Event(cast.ID) (or call the same cleanup callback path used elsewhere) so ActiveCastCleaner, SyncChanges and Destroy/HandleBulkMoveTo logic see and remove these casts; update the Raycast, Blockcast, and Spherecast blocks accordingly and/or attach a termination callback from ParallelSimulation.Register to invoke ActiveCastCleaner and SyncChanges for the cast.src/FastCast2/ActiveCast.luau (1)
367-380:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winFinish the
Trajectoryrename insideSimulateCast().These lines switch to
local trajectory = cast.StateInfo.Trajectory, but the function still readslatestTrajectorylater on. That variable is undefined here, so the first simulation step will nil-index onStartTime/Acceleration.Suggested fix
local trajectory = cast.StateInfo.Trajectory @@ - totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime + totalDelta = cast.StateInfo.TotalRuntime - trajectory.StartTime @@ - if typeof(latestTrajectory.Acceleration) ~= "Vector3" then - latestTrajectory.Acceleration = Vector3.new() + if typeof(trajectory.Acceleration) ~= "Vector3" then + trajectory.Acceleration = Vector3.new() endAlso applies to: 416-418
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/FastCast2/ActiveCast.luau` around lines 367 - 380, In SimulateCast(), you started using local trajectory = cast.StateInfo.Trajectory but left uses of latestTrajectory later; replace every use of latestTrajectory with trajectory (e.g., when computing totalDelta and accessing StartTime/Acceleration) so lines that compute totalDelta, and any subsequent references (also around the block at the other occurrence ~416-418) use trajectory.StartTime, trajectory.Acceleration, etc.; verify GetPositionAtTime/GetVelocityAtTime calls and TotalRuntime updates all reference trajectory instead of latestTrajectory.README.md (1)
200-202:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse
Hit, notRayHit, in the events module example.The runtime lookup uses
FastCastEvents.Hit;module.RayHitwill never be invoked.Suggested fix
-module.RayHit = function() +module.Hit = function() print("Hit!") end🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 200 - 202, The example registers the wrong event name; replace the handler assignment of module.RayHit with the runtime-expected FastCastEvents.Hit by defining module.Hit (or assigning to module[FastCastEvents.Hit]) so the runtime lookup calls it—update the example to use module.Hit instead of module.RayHit and ensure the string/constant FastCastEvents.Hit is referenced consistently with the rest of the module.src/FastCast2/init.luau (2)
128-145:⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy liftUnify the
StateInfo.Trajectoryshape before using it here.This file currently treats
Trajectoryas both an array (trajectories[index],table.insert(...)) and a single segment (trajectory.StartTime). It also writes throughlastTrajectory, which is never defined in this scope. The first transform update on this path will hit a nil access or mutate the wrong structure.Also applies to: 154-190
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/FastCast2/init.luau` around lines 128 - 145, GetTrajectoryInfo and GetLatestTrajectoryEndInfo assume StateInfo.Trajectory is an array but elsewhere it may be a single segment and code writes through lastTrajectory (undefined here), causing nil/mutation bugs; fix by normalizing StateInfo.Trajectory to a consistent array shape before use (e.g., if StateInfo.Trajectory is a map with StartTime, wrap it in a one-element array) and update any writers to always push/replace elements on that array, and remove or properly define lastTrajectory (use the local variable trajectory or StateInfo.Trajectory[#] instead) so GetTrajectoryInfo(GetLatestTrajectoryEndInfo) never indexes a non-array or mutates the wrong structure.
645-651:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReturn after warning when the parallel caster is not initialized.
This guard only logs, then immediately calls
self.Dispatcher:DispatchAll(...)anyway. Ifself.Dispatcheris nil,SetBulkMoveEnabled()still crashes on the next line.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/FastCast2/init.luau` around lines 645 - 651, In SetBulkMoveEnabled, the guard only warns when the caster isn't initialized but proceeds to call self.Dispatcher:DispatchAll and set self.BulkMoveEnabled, causing a crash if self.Dispatcher is nil; update the function (FastCastParallel:SetBulkMoveEnabled) to return immediately after the warn (or otherwise check for self.Dispatcher) so DispatchAll and setting BulkMoveEnabled only run when self.AlreadyInit and self.Dispatcher are valid.
🧹 Nitpick comments (2)
src/FastCast2/Motor6DPool.luau (1)
46-53: 💤 Low valueConsider simplifying
Get()to avoid recursion.The recursive call after
GrowPoolworks correctly but could be simplified with a direct return.Simplified version
local function Get(): Motor6D - if `#FreeMotor6Ds` > 0 then - return table.remove(FreeMotor6Ds) :: Motor6D - else + if `#FreeMotor6Ds` == 0 then GrowPool(PoolSize * GROWTH_RATE) - return Get() end + return table.remove(FreeMotor6Ds) :: Motor6D end🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/FastCast2/Motor6DPool.luau` around lines 46 - 53, The Get function currently recurses after calling GrowPool; replace that recursion with direct logic: call GrowPool(PoolSize * GROWTH_RATE) when FreeMotor6Ds is empty and then immediately remove and return a Motor6D from FreeMotor6Ds. Update the Get function to check FreeMotor6Ds, call GrowPool if empty, then perform a single table.remove(FreeMotor6Ds) :: Motor6D return; keep references to FreeMotor6Ds, GrowPool, PoolSize and GROWTH_RATE intact.Benchmarks/benchSerial.client.luau (1)
55-70: 💤 Low valueConsider adding
ProjectileContainerto filter exclusions.The
FilterDescendantsInstancesis empty, butcastBehavior.AutoIgnoreContainer = trueshould handle this. However, explicitly adding the container ensures it's excluded even before AutoIgnore processes it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Benchmarks/benchSerial.client.luau` around lines 55 - 70, The RaycastParams FilterDescendantsInstances is left empty which can let the ProjectileContainer be hit before AutoIgnoreContainer runs; update CastParams.FilterDescendantsInstances to include the ProjectileContainer instance so the container is explicitly excluded from raycasts (adjust the CastParams used by castBehavior), i.e. add ProjectileContainer to CastParams.FilterDescendantsInstances so CastParams (used by castBehavior.RaycastParams) will proactively ignore that container.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Benchmarks/benchSerial.client.luau`:
- Around line 106-121: The current random direction uses integer
math.random(-1,1) which can produce a zero vector; update the Caster:RaycastFire
call to build float random components instead (e.g. x = (math.random() * 2 - 1)
* 5000) and normalize the Vector3 before passing it, and if the normalized
vector has zero magnitude replace it with a safe fallback direction (e.g.
Vector3.new(0,0,1)); apply these changes where Caster:RaycastFire is invoked so
the direction passed to RaycastFire is never a zero vector and maintains
expected length handling with the 35 parameter and castBehavior.
In `@README.md`:
- Around line 96-100: The README example uses FastCastEnums but never defines or
requires it, so copy-pasting fails; update the example to first require or
define FastCastEnums (the same module providing enums used by FastCast2) before
creating the behavior—e.g., add a line showing how to obtain FastCastEnums (the
module that exposes HighFidelityBehavior) and then keep the existing code that
calls FastCast2.newBehavior() and sets behavior.HighFidelityBehavior =
FastCastEnums.HighFidelityBehavior.Default so the snippet runs as-is.
In `@src/FastCast2/ActiveCast.luau`:
- Around line 854-857: High-fidelity branch still reads from latestTrajectory
instead of the new local trajectory, causing the crash in
HighFidelityBehavior.Always; update the Always branch to use the local variable
trajectory (from cast.StateInfo.Trajectory, where you already default
trajectory.Acceleration) when pulling origin/startTime/velocity/acceleration
(replace references to latestTrajectory in the HighFidelityBehavior.Always code
path with trajectory) so the branch uses the validated trajectory object.
In `@src/FastCast2/ActiveCastSerial.luau`:
- Around line 148-156: The RayInfo table in ActiveCastSerial.luau doesn't copy
shape fields, so SerialSimulation.Register (which reads cast.RayInfo.Size and
cast.RayInfo.Radius for Blockcast/Spherecast) sees nil; update the constructor
that builds RayInfo to include Size = castData.Size and Radius = castData.Radius
(or the appropriate source names on castData) so Blockcast and Spherecast have
their shape parameters when Type is set via CastVariantTypes[castData.CastType];
ensure the names match what SerialSimulation.Register expects.
In `@src/FastCast2/BaseCastSerial.luau`:
- Around line 32-45: The module-level mutable state (Output, ParentCaster,
ObjectCache, BulkMoveToConnection, NextProjectileID) must be made instance-local
so multiple BaseCastSerial instances do not share or clobber each other; modify
BaseCastSerial.Init to set self.Output, self.ParentCaster, self.ObjectCache,
self.BulkMoveToConnection and self.NextProjectileID (and initialize them
appropriately) instead of writing the module-scope variables, then update all
functions in BaseCastSerial that currently reference Output, ParentCaster,
ObjectCache, BulkMoveToConnection or NextProjectileID to use self.Output,
self.ParentCaster, self.ObjectCache, self.BulkMoveToConnection and
self.NextProjectileID so every caster keeps its own event sink/cache/parent and
ID counter.
In `@src/FastCast2/init.luau`:
- Around line 919-935: FastCastSerial:TerminateCast currently only disconnects
local state and destroys cosmetic objects but never delegates termination to the
base serial, leaving the cast registered; update FastCastSerial:TerminateCast to
call self.BaseCast:TerminateCast(cast, castTerminatingFunction) (instead of
directly invoking castTerminatingFunction) after performing the local cleanup
(disconnecting UpdateConnection and destroying CosmeticBulletObject) and before
firing self.Output:Fire("CastTerminating", cast) so the cast is properly
deregistered from SerialSimulation/BaseCast.
- Around line 815-818: GetVelocityCast in FastCastSerial currently returns the
trajectory's InitialVelocity, which ignores acceleration and elapsed time;
update GetVelocityCast(cast: vaildcast) to compute velocity at runtime by using
cast.StateInfo.Trajectory and the elapsed time (use TotalRuntime - StartTime
from the same cast.StateInfo like the position helper does), then evaluate the
trajectory's velocity at that elapsed time and return it instead of
InitialVelocity so accelerating serial casts report the current velocity.
- Around line 847-903: The setters/adders (SetVelocityCast, SetAccelerationCast,
SetPositionCast, AddPositionCast, AddVelocityCast, AddAccelerationCast) mutate
cast.StateInfo.Trajectory in-place without advancing StartTime to the cast's
TotalRuntime, causing retroactive path changes; before changing
Origin/InitialVelocity/Acceleration you must "rebase" the serial trajectory by
integrating the current Trajectory forward to cast.StateInfo.TotalRuntime
(compute the new Origin at that time), set Trajectory.StartTime =
cast.StateInfo.TotalRuntime, and replace
Trajectory.Origin/InitialVelocity/Acceleration with values relative to that new
StartTime (or add deltas for the Add* methods) so subsequent solves treat the
change as occurring only from the current runtime onward. Ensure this rebase
logic is invoked from each of the listed methods (or factor into a helper like
RebaseSerialTrajectory(cast) and call it first).
- Around line 1034-1044: The constructors FastCast.new and FastCast.newParallel
build caster tables (e.g., local fs = { ... } and local fs = { ... }) and set
their metatables (setmetatable(fs, FastCastSerial) / setmetatable(fs,
FastCastParallel)), but currently don't return them; update both functions to
return the constructed fs at the end so callers receive the caster instance
(ensure return fs is added after setmetatable in both FastCast.new and
FastCast.newParallel).
- Around line 721-733: The serial BindableEvent created as BindableOutput is not
wired to the public signals after calling BaseCastSerial.Init, so events like
"CastFire" and "CastTerminating" emitted by self.BaseCast never reach consumers;
add a listener on BindableOutput.Event (after self.BaseCast =
BaseCastSerial.Init(...)) that inspects the incoming event name and forwards it
to the corresponding public signal on this instance (for example, map "CastFire"
-> self.CastFire:Fire(...), "CastTerminating" -> self.CastTerminating:Fire(...),
and forward any other serial event names similarly) so that
FastCastSerial:Init/instance exposes the same callbacks as BaseCastSerial.
In `@src/FastCast2/ParallelSimulation.luau`:
- Around line 263-270: The call to CastHandler is passing a table named variant
for all cast types but Blockcast expects a Vector3 and Spherecast expects a
number; change how variant is constructed so that for EnumCastTypes.Blockcast
you set variant to castSize[i] (a Vector3) and for EnumCastTypes.Spherecast set
variant to castRadius[i] (a number), leaving the existing table form only for
other cast types if needed, then call CastHandler(castWorldRoot[i],
lastPosition, rayDir, castRaycastParams[i], variant).
- Around line 313-316: The loop that calls ParallelSimulation.Terminate over
destroyedIds must iterate in descending numeric order because Unregister() uses
swap-delete and removing lower indices first can change positions of higher
indices; update the code that processes destroyedIds (the block calling
ParallelSimulation.Terminate) to sort or iterate destroyedIds from highest to
lowest (e.g., iterate numeric indices in reverse) so Terminate is invoked on IDs
in descending order to avoid invalidating queued ids after Unregister().
- Around line 161-221: The removal logic swaps the tail (lastId) into slot id
but then disconnects and clears fields from id (corrupting the moved cast); fix
by treating lastId as the source: when id ~= lastId assign casts[id] =
casts[lastId] (in addition to the per-field arrays) and update casts[lastId].ID
= id, then call Motor6DPool.Disconnect(castMotor6D[lastId]) and nil out all
cast*[...] entries using lastId (not id) before decrementing castCount; ensure
castCFrame and castMotor6D clears reference lastId so the moved cast in slot id
remains intact.
- Around line 321-324: ParallelSimulation.Start currently connects RS.PreRender
(client-only) which will error on servers; modify ParallelSimulation.Start to
first check the RunService client/server state (use RS:IsClient() or similar)
and only call RS.PreRender:ConnectParallel(UpdateCasts) when running on the
client, or alternatively use RS.Heartbeat for server-side behavior; ensure you
still set and guard ParallelSimulation.StepConnection and early-return if
already connected. This change keeps ParallelSimulation.Start, UpdateCasts, and
ParallelSimulation.StepConnection as the anchors for the fix.
In `@src/FastCast2/SerialSimulation.luau`:
- Around line 317-320: The loop that calls SerialSimulation.Terminate for each
id in destroyedIds must iterate destroyedIds in descending index order to avoid
invalidation caused by swap-delete compaction; change the iteration over
destroyedIds so you process highest indices first (e.g., iterate from
`#destroyedIds` down to 1) and call SerialSimulation.Terminate(id) in that
descending order to ensure removals do not shift and invalidate later ids.
- Around line 263-274: The code always passes the variant table to CastHandler,
but Blockcast and Spherecast handlers expect a raw Vector3/number; update the
call so it passes variant.Size for EnumCastTypes.Blockcast and variant.Radius
for EnumCastTypes.Spherecast (use castSize[i] and castRadius[i] or the variant
fields you already set) instead of the whole variant table when castType is
Blockcast or Spherecast; keep passing variant only for other cast types. Ensure
this change is applied at the CastHandler(...) invocation so the correct shape
argument is forwarded to block/sphere handlers.
- Around line 164-225: The code in Unregister swaps the tail row into id but
then disconnects and clears entries at index id (which now holds the moved
cast), causing the moved cast to be corrupted; change the cleanup to operate on
lastId instead: perform Motor6DPool.Disconnect(castMotor6D[lastId]) and set
castIDs[lastId], castOrigin[lastId], ..., castCFrame[lastId],
castMotor6D[lastId], casts[lastId] = nil, then set castCount = lastId - 1. Keep
the earlier copy/ID update that moves lastId into id, but ensure all nil
assignments and the disconnect target lastId (not id) so the moved cast at id
remains intact.
---
Outside diff comments:
In `@README.md`:
- Around line 200-202: The example registers the wrong event name; replace the
handler assignment of module.RayHit with the runtime-expected FastCastEvents.Hit
by defining module.Hit (or assigning to module[FastCastEvents.Hit]) so the
runtime lookup calls it—update the example to use module.Hit instead of
module.RayHit and ensure the string/constant FastCastEvents.Hit is referenced
consistently with the rest of the module.
In `@src/FastCast2/ActiveCast.luau`:
- Around line 367-380: In SimulateCast(), you started using local trajectory =
cast.StateInfo.Trajectory but left uses of latestTrajectory later; replace every
use of latestTrajectory with trajectory (e.g., when computing totalDelta and
accessing StartTime/Acceleration) so lines that compute totalDelta, and any
subsequent references (also around the block at the other occurrence ~416-418)
use trajectory.StartTime, trajectory.Acceleration, etc.; verify
GetPositionAtTime/GetVelocityAtTime calls and TotalRuntime updates all reference
trajectory instead of latestTrajectory.
In `@src/FastCast2/BaseCast.luau`:
- Around line 175-191: The new casts (created in the
Raycast/Blockcast/Spherecast paths using ActiveCast.createCastData and then
ParallelSimulation.Register(cast)) must be added into the Actives table and have
their completion trigger the existing cleanup/sync events; after you call
ParallelSimulation.Register(cast) add Actives[cast.ID] = cast, and ensure when
the parallel simulation signals a cast completion you fire
ActiveCastCleaner.Event(cast.ID) (and remove Actives[cast.ID]) and also fire
SyncChanges.Event(cast.ID) (or call the same cleanup callback path used
elsewhere) so ActiveCastCleaner, SyncChanges and Destroy/HandleBulkMoveTo logic
see and remove these casts; update the Raycast, Blockcast, and Spherecast blocks
accordingly and/or attach a termination callback from
ParallelSimulation.Register to invoke ActiveCastCleaner and SyncChanges for the
cast.
In `@src/FastCast2/init.luau`:
- Around line 128-145: GetTrajectoryInfo and GetLatestTrajectoryEndInfo assume
StateInfo.Trajectory is an array but elsewhere it may be a single segment and
code writes through lastTrajectory (undefined here), causing nil/mutation bugs;
fix by normalizing StateInfo.Trajectory to a consistent array shape before use
(e.g., if StateInfo.Trajectory is a map with StartTime, wrap it in a one-element
array) and update any writers to always push/replace elements on that array, and
remove or properly define lastTrajectory (use the local variable trajectory or
StateInfo.Trajectory[#] instead) so
GetTrajectoryInfo(GetLatestTrajectoryEndInfo) never indexes a non-array or
mutates the wrong structure.
- Around line 645-651: In SetBulkMoveEnabled, the guard only warns when the
caster isn't initialized but proceeds to call self.Dispatcher:DispatchAll and
set self.BulkMoveEnabled, causing a crash if self.Dispatcher is nil; update the
function (FastCastParallel:SetBulkMoveEnabled) to return immediately after the
warn (or otherwise check for self.Dispatcher) so DispatchAll and setting
BulkMoveEnabled only run when self.AlreadyInit and self.Dispatcher are valid.
---
Nitpick comments:
In `@Benchmarks/benchSerial.client.luau`:
- Around line 55-70: The RaycastParams FilterDescendantsInstances is left empty
which can let the ProjectileContainer be hit before AutoIgnoreContainer runs;
update CastParams.FilterDescendantsInstances to include the ProjectileContainer
instance so the container is explicitly excluded from raycasts (adjust the
CastParams used by castBehavior), i.e. add ProjectileContainer to
CastParams.FilterDescendantsInstances so CastParams (used by
castBehavior.RaycastParams) will proactively ignore that container.
In `@src/FastCast2/Motor6DPool.luau`:
- Around line 46-53: The Get function currently recurses after calling GrowPool;
replace that recursion with direct logic: call GrowPool(PoolSize * GROWTH_RATE)
when FreeMotor6Ds is empty and then immediately remove and return a Motor6D from
FreeMotor6Ds. Update the Get function to check FreeMotor6Ds, call GrowPool if
empty, then perform a single table.remove(FreeMotor6Ds) :: Motor6D return; keep
references to FreeMotor6Ds, GrowPool, PoolSize and GROWTH_RATE intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7a57e535-492e-4838-826f-86da925a99ab
📒 Files selected for processing (18)
Benchmarks/benchParallel.client.luauBenchmarks/benchSerial.client.luauREADME.mdTODO.mddocs/api-reference.mddocs/changelog.mdsrc/FastCast2/ActiveCast.luausrc/FastCast2/ActiveCastSerial.luausrc/FastCast2/BaseCast.luausrc/FastCast2/BaseCastSerial.luausrc/FastCast2/Configs.luausrc/FastCast2/DefaultConfigs.luausrc/FastCast2/FastCastEnums.luausrc/FastCast2/Motor6DPool.luausrc/FastCast2/ParallelSimulation.luausrc/FastCast2/SerialSimulation.luausrc/FastCast2/TypeDefinitions.luausrc/FastCast2/init.luau
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 45265
FastCast2 AI Agent Prompt — Implement HighFidelitySegmentSize & HighFidelityBehavior in SoA Simulations
Background: how high-fidelity modes work (from
|
| Enum | Behaviour |
|---|---|
Default = 1 |
No subdivision — current behaviour, one cast per frame |
Automatic = 2 |
One coarse cast per frame; if a hit is detected, subdivide the displacement into floor(rayDisplacement / HighFidelitySegmentSize) sub-segments and re-cast each one to find the precise hit point |
Always = 3 |
Every frame, estimate the frame displacement, subdivide the full deltaTime into floor(displacement / HighFidelitySegmentSize) sub-segments, and simulate each one individually |
Both modes share these details:
numSegmentsReal = max(1, floor(rayDisplacement / segmentSize))timeIncrement = deltaTime / numSegmentsReal- A
castIsActivelyResimulating[i]guard prevents cascading lag — if already resimulating when re-entered, terminate + warn. - A
castCancelHighResCast[i]flag can break out of the sub-loop early (set externally, e.g. bySyncChangesToCast).
Shared helpers needed (add near the top of each file, alongside GetPositionAtTime/GetVelocityAtTime)
Both files already have GetPositionAtTime and GetVelocityAtTime. No new helpers are needed — the sub-loop inlines the same kinematics.
Fix — Replace UpdateCasts inner loop body in both simulation files
Replace the existing per-cast block inside the for i = 1, castCount do loop with the following. The only difference between the two files is the Terminate call at the end (SerialSimulation.Terminate vs ParallelSimulation.Terminate). The template below uses ThisSimulation as a placeholder — substitute accordingly.
local function UpdateCasts(deltaTime: number)
if castCount == 0 then return end
local destroyedIds = {} :: { number }
for i = 1, castCount do
if castPaused[i] then continue end
local caster = castCaster[i]
local castType = castCastType[i]
local CastHandler = castHandlers[castType]
local origin = castOrigin[i]
local velocity = castVelocity[i]
local acceleration = castAcceleration[i]
-- Resolve the raw size/radius value (not a table)
local sizeOrRadius: (Vector3 | number)?
if castType == EnumCastTypes.Blockcast then
sizeOrRadius = castSize[i]
elseif castType == EnumCastTypes.Spherecast then
sizeOrRadius = castRadius[i]
end
local hifiBehavior = castHighFidelityBehavior[i]
local hifiSegmentSize = castHighFidelitySegmentSize[i]
local bullet = castCosmeticBullet[i]
local motor6d = castMotor6D[i]
-- ──────────────────────────────────────────────────────────────
-- HighFidelityBehavior.Always (3):
-- Subdivide every frame into sub-segments before casting.
-- ──────────────────────────────────────────────────────────────
if hifiBehavior == 3 and hifiSegmentSize > 0 then
-- Cascading lag guard
if castIsActivelyResimulating[i] then
table.insert(destroyedIds, i)
warn("Cascading cast lag encountered on cast " .. i .. "! Consider increasing HighFidelitySegmentSize.")
continue
end
castIsActivelyResimulating[i] = true
-- Estimate frame displacement to compute segment count
local totalDeltaForEstimate = castTotalRuntime[i]
local posNow = GetPositionAtTime(totalDeltaForEstimate, origin, velocity, acceleration)
local velNow = GetVelocityAtTime(totalDeltaForEstimate, velocity, acceleration)
local posNext = GetPositionAtTime(totalDeltaForEstimate + deltaTime, origin, velocity, acceleration)
local estimatedDisplacement = (posNext - posNow).Magnitude
-- Estimate via coarse cast to see if we even need subdivision
local coarseRayDir = (posNext - posNow).Unit * velNow.Magnitude * deltaTime
local coarseResult = CastHandler(castWorldRoot[i], posNow, coarseRayDir, castRaycastParams[i], sizeOrRadius)
if coarseResult then
estimatedDisplacement = (coarseResult.Position - posNow).Magnitude
end
local numSegmentsReal = math.max(1, math.floor(estimatedDisplacement / hifiSegmentSize))
local timeIncrement = deltaTime / numSegmentsReal
local hitScheduled = false
for segIndex = 1, numSegmentsReal do
if castCancelHighResCast[i] then
castCancelHighResCast[i] = false
break
end
local preDelta = castTotalRuntime[i]
local lastPoint = GetPositionAtTime(preDelta, origin, velocity, acceleration)
castTotalRuntime[i] += timeIncrement
local nowDelta = castTotalRuntime[i]
local currentPosition = GetPositionAtTime(nowDelta, origin, velocity, acceleration)
local currentVelocity = GetVelocityAtTime(nowDelta, velocity, acceleration)
local displacement = currentPosition - lastPoint
local rayDir = displacement.Unit * currentVelocity.Magnitude * timeIncrement
local result = CastHandler(castWorldRoot[i], lastPoint, rayDir, castRaycastParams[i], sizeOrRadius)
local hitPoint = currentPosition
local hitPart = nil
if result then
hitPoint = result.Position
hitPart = result.Instance
end
local rayDisplacement = (hitPoint - lastPoint).Magnitude
castDistanceCovered[i] += rayDisplacement
local newCFrame = CFrame.new(lastPoint, lastPoint + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2)
castCFrame[i] = newCFrame
-- Move cosmetic bullet
if bullet then
if motor6d then
motor6d.Transform = newCFrame
elseif bullet:IsA("BasePart") then
bullet.CFrame = newCFrame
else
bullet:PivotTo(newCFrame)
end
end
QueueFire(caster, "LengthChanged", casts[i], lastPoint, rayDir.Unit, rayDisplacement, currentVelocity, bullet)
if result and hitPart ~= bullet then
QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet)
hitScheduled = true
break
end
if castDistanceCovered[i] >= castMaxDistance[i] then
hitScheduled = true
break
end
end
castIsActivelyResimulating[i] = false
if hitScheduled then
table.insert(destroyedIds, i)
end
-- ──────────────────────────────────────────────────────────────
-- HighFidelityBehavior.Automatic (2):
-- Do one coarse cast; on hit, subdivide for precision.
-- ──────────────────────────────────────────────────────────────
elseif hifiBehavior == 2 and hifiSegmentSize > 0 then
-- Coarse cast (same as Default)
local preDelta = castTotalRuntime[i]
local lastPoint = GetPositionAtTime(preDelta, origin, velocity, acceleration)
castTotalRuntime[i] += deltaTime
local nowDelta = castTotalRuntime[i]
local currentPosition = GetPositionAtTime(nowDelta, origin, velocity, acceleration)
local currentVelocity = GetVelocityAtTime(nowDelta, velocity, acceleration)
local displacement = currentPosition - lastPoint
local rayDir = displacement.Unit * currentVelocity.Magnitude * deltaTime
local result = CastHandler(castWorldRoot[i], lastPoint, rayDir, castRaycastParams[i], sizeOrRadius)
local hitPoint = currentPosition
local hitPart = nil
if result then
hitPoint = result.Position
hitPart = result.Instance
end
local rayDisplacement = (hitPoint - lastPoint).Magnitude
castDistanceCovered[i] += rayDisplacement
local newCFrame = CFrame.new(lastPoint, lastPoint + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2)
castCFrame[i] = newCFrame
if bullet then
if motor6d then
motor6d.Transform = newCFrame
elseif bullet:IsA("BasePart") then
bullet.CFrame = newCFrame
else
bullet:PivotTo(newCFrame)
end
end
QueueFire(caster, "LengthChanged", casts[i], lastPoint, rayDir.Unit, rayDisplacement, currentVelocity, bullet)
-- On coarse hit → refine with sub-segments
if result and hitPart ~= bullet then
-- Cascading lag guard
if castIsActivelyResimulating[i] then
QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet)
table.insert(destroyedIds, i)
warn("Cascading cast lag encountered on cast " .. i .. "! Consider increasing HighFidelitySegmentSize.")
continue
end
castIsActivelyResimulating[i] = true
local numSegmentsReal = math.max(1, math.floor(rayDisplacement / hifiSegmentSize))
local timeIncrement = deltaTime / numSegmentsReal
-- Rewind TotalRuntime to re-simulate sub-segments from preDelta
castTotalRuntime[i] = preDelta
-- Also rewind distance contribution for re-counting
castDistanceCovered[i] -= rayDisplacement
local preciseHit = false
for segIndex = 1, numSegmentsReal do
if castCancelHighResCast[i] then
castCancelHighResCast[i] = false
break
end
local subPreDelta = castTotalRuntime[i]
local subLastPoint = GetPositionAtTime(subPreDelta, origin, velocity, acceleration)
castTotalRuntime[i] += timeIncrement
local subNowDelta = castTotalRuntime[i]
local subCurrentPosition = GetPositionAtTime(subNowDelta, origin, velocity, acceleration)
local subCurrentVelocity = GetVelocityAtTime(subNowDelta, velocity, acceleration)
local subDisplacement = subCurrentPosition - subLastPoint
local subRayDir = subCurrentVelocity * timeIncrement
local subResult = CastHandler(castWorldRoot[i], subLastPoint, subRayDir, castRaycastParams[i], sizeOrRadius)
local subHitPoint = subCurrentPosition
local subHitPart = nil
if subResult then
subHitPoint = subResult.Position
subHitPart = subResult.Instance
end
local subRayDisplacement = (subHitPoint - subLastPoint).Magnitude
castDistanceCovered[i] += subRayDisplacement
local subCFrame = CFrame.new(subLastPoint, subLastPoint + subRayDir) * CFrame.new(0, 0, -subRayDisplacement / 2)
castCFrame[i] = subCFrame
if bullet then
if motor6d then
motor6d.Transform = subCFrame
elseif bullet:IsA("BasePart") then
bullet.CFrame = subCFrame
else
bullet:PivotTo(subCFrame)
end
end
if subResult and subHitPart ~= bullet then
QueueFire(caster, "Hit", casts[i], subResult, subCurrentVelocity, bullet)
preciseHit = true
break
end
if castDistanceCovered[i] >= castMaxDistance[i] then
preciseHit = true
break
end
end
castIsActivelyResimulating[i] = false
-- If sub-loop found no hit (can pierce case / numeric edge), don't terminate
if preciseHit then
table.insert(destroyedIds, i)
end
elseif castDistanceCovered[i] >= castMaxDistance[i] then
table.insert(destroyedIds, i)
end
-- ──────────────────────────────────────────────────────────────
-- HighFidelityBehavior.Default (1) — unchanged behaviour
-- ──────────────────────────────────────────────────────────────
else
local preDelta = castTotalRuntime[i]
local lastPoint = GetPositionAtTime(preDelta, origin, velocity, acceleration)
castTotalRuntime[i] += deltaTime
local nowDelta = castTotalRuntime[i]
local currentPosition = GetPositionAtTime(nowDelta, origin, velocity, acceleration)
local currentVelocity = GetVelocityAtTime(nowDelta, velocity, acceleration)
local displacement = currentPosition - lastPoint
local rayDir = displacement.Unit * currentVelocity.Magnitude * deltaTime
local result = CastHandler(castWorldRoot[i], lastPoint, rayDir, castRaycastParams[i], sizeOrRadius)
local hitPoint = currentPosition
local hitPart = nil
if result then
hitPoint = result.Position
hitPart = result.Instance
end
local rayDisplacement = (hitPoint - lastPoint).Magnitude
castDistanceCovered[i] += rayDisplacement
local newCFrame = CFrame.new(lastPoint, lastPoint + rayDir) * CFrame.new(0, 0, -rayDisplacement / 2)
castCFrame[i] = newCFrame
if bullet then
if motor6d then
motor6d.Transform = newCFrame
elseif bullet:IsA("BasePart") then
bullet.CFrame = newCFrame
else
bullet:PivotTo(newCFrame)
end
end
QueueFire(caster, "LengthChanged", casts[i], lastPoint, rayDir.Unit, rayDisplacement, currentVelocity, bullet)
if result and hitPart ~= bullet then
QueueFire(caster, "Hit", casts[i], result, currentVelocity, bullet)
table.insert(destroyedIds, i)
elseif castDistanceCovered[i] >= castMaxDistance[i] then
table.insert(destroyedIds, i)
end
end
end
-- Terminate in reverse to avoid swap-with-last index invalidation
for i = `#destroyedIds`, 1, -1 do
ThisSimulation.Terminate(destroyedIds[i]) -- replace with SerialSimulation or ParallelSimulation
end
DispatchAllEvents()
endSummary of what changed vs. current UpdateCasts
| Change | Reason |
|---|---|
Branch on castHighFidelityBehavior[i] |
Route to correct simulation mode |
Always (3): estimate displacement → subdivide deltaTime → simulate each sub-segment |
Every-frame precision |
Automatic (2): coarse cast → on hit, rewind TotalRuntime and DistanceCovered, then subdivide for precise hit |
On-demand precision only when needed |
castIsActivelyResimulating[i] guard in both HiFi paths |
Prevents cascading lag |
castCancelHighResCast[i] checked per sub-segment iteration |
Allows external early-exit |
Raw sizeOrRadius passed to CastHandler (not a table) |
Fixes pre-existing bug (Fix 13) |
| Default (1): identical to current code | No regression |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainLet me read the simulation files and the existing high-fidelity logic in 🏁 Script executed: Length of output: 54537
FastCast2 AI Agent Prompt — Implement HighFidelitySegmentSize & HighFidelityBehavior in SerialSimulation and ParallelSimulationBoth Reference the existing implementation in
Step 1 — Add the
|
| Concern | Handled |
|---|---|
subRayDir = subVelocity * timeIncrement (not unit * magnitude * dt) |
✅ matches ActiveCast.luau fix |
Cascading lag guard for both Always and Automatic modes |
✅ |
CancelHighResCast flag respected in PerformHighFidelitySubcasts |
✅ |
castTotalRuntime[i] advanced inside sub-cast loop so distance/position stay consistent |
✅ |
destroyedIds deduplication — same index could be inserted twice (hit + maxDistance in Always) |
if not table.find(destroyedIds, i) then table.insert(destroyedIds, i) end in both the hit and maxDistance checks inside PerformHighFidelitySubcasts |
…A simulations - Add HighFidelityBehavior.Automatic subcast verification in UpdateCasts - When ray displacement exceeds segment size, perform subcast re-validation - Update bullet position to accurate hit point after subcast verification - Applied to both ParallelSimulation and SerialSimulation (SoA pattern)
- Add castCanPierceFn array for storing pierce callback functions - Support piercing in both normal hit handling and HighFidelity subcasts - Fire Pierced event when CanPierce returns true, continue simulation - Fire Hit event when CanPierce returns false or is nil - Applied to both ParallelSimulation and SerialSimulation
- Add castEventsConfig, castEventsModuleConfig, castEventsModule SoA arrays - Update QueueFire to check both FastCastEventsConfig and FastCastEventsModuleConfig - Fire module callbacks directly when FastCastEventsModule is set - Fire CastFire, CastTerminating events with proper gating
CastFire should fire: (cast, origin, direction, velocity, behavior) Not: (cast, origin, velocity, acceleration)
Comment out the SimulateCast and Stepped functions for future reference.
Add DebugLogging checks for: - Casting (per frame) - Hit detection - RayPierce (piercing function returns, no function set) - Calculation (subcast info) - Segment (per-segment subcast iteration) Matches debug output in legacy ActiveCast.luau
Add visualization functions for ray/block/sphere segments and hit markers: - VisualizeRaySegment, VisualizeBlockSegment, VisualizeSphereSegment - VisualizeHit with pierce/hit color differentiation - Uses task.synchronize in ParallelSimulation for cross-thread safety - Visualize cast segments per frame, hit markers on hit/pierce events - Matches legacy Debug_Segment/Hit/RayPierce colors
|
@coderabbitai summary |
✅ Actions performedSummary regeneration triggered. |
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Chores